package org.tamal.java;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * This class iterates over the class path classes.
 * @author Tamal Kanti Nath
 */
public final class ClassFinder implements Iterable<String> {

    private final List<File> dirs = new ArrayList<>();
    private final List<JarFile> jars = new ArrayList<>();

    /**
     * Returns ClassFinder instance with boot, extension and system class path.
     */
    public ClassFinder() {
        final Set<String> path = new TreeSet<>();
        String p = System.getProperty("sun.boot.class.path");
        path.addAll(Arrays.asList(p.split(File.pathSeparator)));
        p = System.getProperty("java.ext.dirs");
        for (String dir: p.split(File.pathSeparator)) {
            String[] array = new File(dir).list();
            if (array != null) {
                path.addAll(Arrays.asList(array));
            }
        }
        p = System.getProperty("java.class.path");
        path.addAll(Arrays.asList(p.split(File.pathSeparator)));
        for (String cp : path) {
            addClassPath(cp);
        }
    }

    /**
     * Returns ClassFinder instance given class path.
     * @param path the class paths
     */
    public ClassFinder(String... path) {
        for (String cp : path) {
            addClassPath(cp);
        }
    }

    /**
     * Adds custom class path or jar file.
     * @param cp the external class path or jar file
     * @return false if class path is invalid or already added, true otherwise
     */
    public boolean addClassPath(String cp) {
        File file = new File(cp);
        if (file.isDirectory()) {
            return dirs.add(file);
        }
        try {
            return jars.add(new JarFile(cp));
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Checks if the identifier is valid or not.
     * @param identifier the identifier name
     * @return true if valid, false otherwise
     */
    public static boolean isValidIdentifier(String identifier) {
        final String[] reserved = {"abstract", "assert", "boolean", "break",
            "byte", "case", "catch", "char", "class", "const", "continue",
            "default", "do", "double", "else", "enum", "extends", "false",
            "final", "finally", "float", "for", "goto", "if", "implements",
            "import", "instanceof", "int", "interface", "long", "native", "new",
            "null", "package", "private", "protected", "public", "return",
            "short", "static", "strictfp", "super", "switch", "synchronized",
            "this", "throw", "throws", "transient", "true", "try", "void",
            "volatile", "while"};
        boolean valid = true;
        if (!Character.isJavaIdentifierStart(identifier.charAt(0))) {
            valid = false;
        }
        if (valid) {
            for (char ch : identifier.toCharArray()) {
                if (!Character.isJavaIdentifierPart(ch)) {
                    valid = false;
                    break;
                }
            }
        }
        if (valid) {
            for (String word : reserved) {
                if (word.equals(identifier)) {
                    valid = false;
                    break;
                }
            }
        }
        return valid;
    }

    @Override
    public Iterator<String> iterator() {
        return new Itr(dirs, jars);
    }

    /**
     * The Iterator for ClassFinder.
     * @author Tamal Kanti Nath
     */
    private class Itr implements Iterator<String> {

        private static final String EXT = ".class";
        private String className;
        private Iterator<File> dirIterator;
        private Iterator<JarFile> jarIterator;
        private Queue<File> dirQueue = new LinkedList<>();
        private File cp;
        private JarFile jarFile;
        private Enumeration<JarEntry> jarEntries;

        Itr(List<File> dirs, List<JarFile> jars) {
            dirIterator = dirs.iterator();
            jarIterator = jars.iterator();
            jarFile = jarIterator.next();
            jarEntries = jarFile.entries();
        }

        @Override
        public boolean hasNext() {
            if (className == null) {
                try {
                    className = nextDirClass();
                } catch (NoSuchElementException e) {
                    className = null;
                }
            }
            if (className == null) {
                try {
                    className = nextJarClass();
                } catch (NoSuchElementException e) {
                    className = null;
                }
            }
            if (className == null) {
                return false;
            }
            return true;
        }

        @Override
        public String next() {
            hasNext();
            String temp = className;
            className = null;
            return temp;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

        private String nextDirClass() {
            File file;
            do {
                if (dirQueue.isEmpty()) {
                    cp = dirIterator.next();
                    dirQueue.add(cp);
                }
                file = dirQueue.remove();
                if (file.isDirectory()) {
                    for (File f : file.listFiles()) {
                        String name = f.getName();
                        if (f.isDirectory() && isValidIdentifier(name)) {
                            dirQueue.add(f);
                        }
                        if (f.isFile() && name.endsWith(EXT)) {
                            name = name.substring(0, name.length() - 6);
                            if (isValidIdentifier(name)) {
                                dirQueue.add(f);
                            }
                        }
                    }
                }
            } while (file.isDirectory());
            String n = file.getAbsolutePath();
            n = n.substring(cp.getAbsolutePath().length() + 1, n.length() - 6);
            return n.replace(File.separatorChar, '.');
        }

        private String nextJarClass() {
            while (true) {
                while (!jarEntries.hasMoreElements()) {
                    try {
                        jarFile.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    jarFile = jarIterator.next();
                    jarEntries = jarFile.entries();
                }
                JarEntry entry = jarEntries.nextElement();
                if (isValidClass(entry)) {
                    String url = entry.getName().replace('/', '.');
                    return url.substring(0, url.length() - 6);
                }
            }
        }

        private boolean isValidClass(JarEntry entry) {
            String name = entry.getName();
            if (!entry.isDirectory() && name.endsWith(EXT)) {
                name = name.substring(0, name.length() - 6);
                boolean valid = true;
                for (String identifier : name.split("/")) {
                    if (!isValidIdentifier(identifier)) {
                        valid = false;
                        break;
                    }
                }
                return valid;
            }
            return false;
        }
    }
}
